#pragma once

//some good values for block size and count
#define BLOCK_SIZE 8192
#define BLOCK_COUNT 20

namespace VirtualPedal
{
	using namespace System::Collections;
	using namespace System::Collections::Generic;
	using namespace System::Runtime::InteropServices;
	using namespace System::Threading;

	//function prototypes
	static void CALLBACK waveOutProc(HWAVEOUT, UINT, DWORD, DWORD, DWORD);
	ref class AudioRenderer;

	class AudioRendererProxy
	{
	public:
		gcroot<AudioRenderer^> ManagedObject;
	};

	public ref class AudioRenderer sealed : public AudioDeviceBase
	{
		List<DeviceDescriptor^>^ m_DeviceNames;
		HWAVEOUT hWaveOut;	//device handle
		WAVEHDR* waveBlocks;
		volatile int waveFreeBlockCount;
		int waveCurrentBlock;

		bool m_bInLoop;
		bool m_bRenderingLoopFlag;
		Thread^ m_RenderingThread;
		AudioPort^ m_DataPort;

		AudioRendererProxy* m_pProxy;

	public:
		virtual property List<DeviceDescriptor^>^ Devices {
			virtual List<DeviceDescriptor^>^ get() override { return m_DeviceNames; }
		}

		void SetDataPort(AudioPort^ port)
		{
			m_DataPort = port;
		}

		virtual property bool IsOpened
		{
			virtual bool get() override { return m_bInLoop; }
		}

	public:

		AudioRenderer()
		{
			hWaveOut = NULL;
			m_bInLoop = false;
			m_bRenderingLoopFlag = false;
			m_pProxy = new AudioRendererProxy();
			m_pProxy->ManagedObject = this;
		}

		~AudioRenderer()
		{
			if(m_pProxy != nullptr)
			{
				delete m_pProxy;
			}
		}

		virtual void Init() override
		{
			m_DeviceNames = gcnew List<DeviceDescriptor^>();

			HRESULT hr = S_OK;
			UINT count = waveOutGetNumDevs();

			for(UINT i=0; i<count; i++)
			{
				WAVEOUTCAPS caps;
				MMRESULT mmr = waveOutGetDevCaps(i, &caps, sizeof(caps));
				if(MMSYSERR_NOERROR != mmr)
				{
					ThrowOnError(E_FAIL);
				}

				DeviceDescriptor^ desc = gcnew DeviceDescriptor();
				desc->Name = gcnew String(caps.szPname);
				desc->FriendlyName = gcnew String(caps.szPname);
				m_DeviceNames->Add(desc);
			}
		}

		virtual void Open(DeviceDescriptor^ desc) override
		{
			if(m_DataPort == nullptr || m_bInLoop)
				return;

			HRESULT hr = S_OK;
			UINT count = waveOutGetNumDevs();
			UINT_PTR uDeviceId = WAVE_MAPPER;

			for(UINT i=0; i<count; i++)
			{
				WAVEOUTCAPS caps;
				MMRESULT mmr = waveOutGetDevCaps(i, &caps, sizeof(caps));
				if(MMSYSERR_NOERROR != mmr)
				{
					ThrowOnError(E_FAIL);
				}
				String^ strId = gcnew String(caps.szPname);
				if(strId == desc->Name)
				{
					//FIXME
					uDeviceId = i;
					break;
				}
			}

			//WAVEFORMATEX wfx;			//look this up in your documentation
			WAVEFORMATEX wfx;			//look this up in your documentation
	
			waveBlocks = allocateBlocks(BLOCK_SIZE, BLOCK_COUNT);
			waveFreeBlockCount = BLOCK_COUNT;
			waveCurrentBlock= 0;

			WAVEFORMATEXTENSIBLE wfext;
			m_DataPort->Descriptor->GetWAVEFORMATEXTENSIBLE(&wfext);
			wfx = wfext.Format;
			wfx.cbSize = 0;

			if(wfext.SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
			{
				wfx.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
			}
			else if(wfext.SubFormat == KSDATAFORMAT_SUBTYPE_PCM)
			{
				wfx.wFormatTag = WAVE_FORMAT_PCM;
			}
			else
			{
				ThrowOnError(E_FAIL);
			}

			//m_DataPort->Descriptor->GetWAVEFORMATEXTENSIBLE(&wfx);

			DWORD_PTR pProxy = (DWORD_PTR)m_pProxy;
			HWAVEOUT hWaveOutTmp;
			
			MMRESULT mmr = waveOutOpen(
				&hWaveOutTmp,
				uDeviceId,
				&wfx,
				(DWORD_PTR)waveOutProc, 
				(DWORD_PTR)pProxy,
				CALLBACK_FUNCTION | WAVE_MAPPED
				);

			hWaveOut = hWaveOutTmp;

			if(mmr != MMSYSERR_NOERROR)
			{
				ThrowOnError(E_FAIL);
			}

			m_bInLoop = true;
			m_bRenderingLoopFlag = true;

			ThreadStart^ ts = gcnew ThreadStart(this, &AudioRenderer::AudioRenderProc);
			m_RenderingThread = gcnew Thread(ts);
			m_RenderingThread->Start();

			ThrowOnError(hr);
		}

		virtual void Close() override
		{
			if(!m_bInLoop)
			{
				return;
			}
			m_bRenderingLoopFlag = false;
			
			m_RenderingThread->Join(System::TimeSpan::FromSeconds(2.0));

			//unprepare any blocks that are still prepared
			for(int i = 0; i < waveFreeBlockCount; i++) 
			{
				if(waveBlocks[i].dwFlags & WHDR_PREPARED)
				{
					waveOutUnprepareHeader(hWaveOut, &waveBlocks[i], sizeof(WAVEHDR));
				}
			}
			waveFreeBlockCount = BLOCK_COUNT;
			Thread::Sleep(System::TimeSpan::FromSeconds(2));
			freeBlocks(waveBlocks);
			waveOutClose(hWaveOut);
		}

	private:

		void AudioRenderProc()
		{
			const DWORD DEVICE_TIME_OUT_TIME = 2000;
			BYTE buffer[1024];
			try
			{
				if(!m_bInLoop)
				{
					return;
				}

				while(m_DataPort->DataBuffer->Read(buffer, sizeof(buffer)) > 0) 
				{
				}

				//playback loop
				while(m_bRenderingLoopFlag)
				{
					Int32 numRead = m_DataPort->DataBuffer->Read(buffer, sizeof(buffer));
					if(numRead != 0)
					{
						writeAudio(hWaveOut, (LPSTR)buffer, numRead);
					}
				}

				//wait for all blocks to complete
				while(waveFreeBlockCount < BLOCK_COUNT)
					Sleep(10);
			}
			catch (System::Threading::ThreadAbortException^)
			{

			}
			finally
			{
				m_bInLoop = false;
			}
		}

	internal:
		void IncreaseFreeBlock()
		{
			Monitor::Enter(this);
			try
			{
				waveFreeBlockCount++;
			}
			finally
			{
				Monitor::Exit(this);
			}
		}

		void writeAudio(HWAVEOUT hWaveOut, LPSTR data, int size)
		{
			WAVEHDR* current;
			int remain;
			current = &waveBlocks[waveCurrentBlock];
			while(size > 0)
			{
				/* 
				 * first make sure the header we're going to use is unprepared
				 */
				if(current->dwFlags & WHDR_PREPARED) 
					waveOutUnprepareHeader(hWaveOut, current, sizeof(WAVEHDR));
				if(size < (int)(BLOCK_SIZE - current->dwUser))
				{
					memcpy(current->lpData + current->dwUser, data, size);
					current->dwUser += size;
					break;
				}
				remain = BLOCK_SIZE - current->dwUser;
				memcpy(current->lpData + current->dwUser, data, remain);
				size -= remain;
				data += remain;
				current->dwBufferLength = BLOCK_SIZE;
				waveOutPrepareHeader(hWaveOut, current, sizeof(WAVEHDR));
				waveOutWrite(hWaveOut, current, sizeof(WAVEHDR));
				
				Monitor::Enter(this);
				try
				{
					waveFreeBlockCount--;
				}
				finally
				{
					Monitor::Exit(this);
				}
				//wait for a block to become free
				while(!waveFreeBlockCount)
					Sleep(10);
				//point to the next block
				waveCurrentBlock++;
				waveCurrentBlock %= BLOCK_COUNT;
				current = &waveBlocks[waveCurrentBlock];
				current->dwUser = 0;
			}
		}

		WAVEHDR* allocateBlocks(int size, int count)
		{
			BYTE* buffer;
			int i;
			WAVEHDR* blocks;
			DWORD totalBufferSize = (size + sizeof(WAVEHDR)) * count;
			
			//allocate memory for the entire set in one go
			buffer = (BYTE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, totalBufferSize );
			if(buffer == NULL)
			{
				ThrowOnError(E_FAIL);
			}

			//and set up the pointers to each bit
			blocks = (WAVEHDR*)buffer;
			buffer += sizeof(WAVEHDR) * count;
			for(i = 0; i < count; i++)
			{
				blocks[i].dwBufferLength = size;
				blocks[i].lpData = (LPSTR)buffer;
				buffer += size;
			}
			return blocks;
		}

		void freeBlocks(WAVEHDR* blockArray)
		{
			//and this is why allocateBlocks works the way it does
			HeapFree(GetProcessHeap(), 0, blockArray);
		}
	};

	static void CALLBACK waveOutProc(
		HWAVEOUT hWaveOut, 
		UINT uMsg, 
		DWORD dwInstance, 
		DWORD dwParam1,
		DWORD dwParam2)
	{
		//pointer to free block counter
		AudioRendererProxy* renderer = (AudioRendererProxy*)dwInstance;

		//ignore calls that occur due to openining and closing the device.
		if(uMsg != WOM_DONE)
			return;
		
		renderer->ManagedObject->IncreaseFreeBlock();
	}
}

